/**
 * Copyright 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule ReactPropTypes
 */

'use strict'

var ReactElement = require('./ReactElement')
var ReactPropTypeLocationNames = require('./ReactPropTypeLocationNames')

var emptyFunction = require('fbjs/lib/emptyFunction')
var getIteratorFn = require('./getIteratorFn')

/**
 * Collection of methods that allow declaration and validation of props that are
 * supplied to React components. Example usage:
 *
 *   var Props = require('ReactPropTypes');
 *   var MyArticle = React.createClass({
 *     propTypes: {
 *       // An optional string prop named "description".
 *       description: Props.string,
 *
 *       // A required enum prop named "category".
 *       category: Props.oneOf(['News','Photos']).isRequired,
 *
 *       // A prop named "dialog" that requires an instance of Dialog.
 *       dialog: Props.instanceOf(Dialog).isRequired
 *     },
 *     render: function() { ... }
 *   });
 *
 * A more formal specification of how these methods are used:
 *
 *   type := array|bool|func|object|number|string|oneOf([...])|instanceOf(...)
 *   decl := ReactPropTypes.{type}(.isRequired)?
 *
 * Each and every declaration produces a function with the same signature. This
 * allows the creation of custom validation functions. For example:
 *
 *  var MyLink = React.createClass({
 *    propTypes: {
 *      // An optional string or URI prop named "href".
 *      href: function(props, propName, componentName) {
 *        var propValue = props[propName];
 *        if (propValue != null && typeof propValue !== 'string' &&
 *            !(propValue instanceof URI)) {
 *          return new Error(
 *            'Expected a string or an URI for ' + propName + ' in ' +
 *            componentName
 *          );
 *        }
 *      }
 *    },
 *    render: function() {...}
 *  });
 *
 * @internal
 */

var ANONYMOUS = '<<anonymous>>'

var ReactPropTypes = {
  array: createPrimitiveTypeChecker('array'),
  bool: createPrimitiveTypeChecker('boolean'),
  func: createPrimitiveTypeChecker('function'),
  number: createPrimitiveTypeChecker('number'),
  object: createPrimitiveTypeChecker('object'),
  string: createPrimitiveTypeChecker('string'),

  any: createAnyTypeChecker(),
  arrayOf: createArrayOfTypeChecker,
  element: createElementTypeChecker(),
  instanceOf: createInstanceTypeChecker,
  node: createNodeChecker(),
  objectOf: createObjectOfTypeChecker,
  oneOf: createEnumTypeChecker,
  oneOfType: createUnionTypeChecker,
  shape: createShapeTypeChecker
}

/**
 * inlined Object.is polyfill to avoid requiring consumers ship their own
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
 */
/*eslint-disable no-self-compare*/
function is(x, y) {
  // SameValue algorithm
  if (x === y) {
    // Steps 1-5, 7-10
    // Steps 6.b-6.e: +0 != -0
    return x !== 0 || 1 / x === 1 / y
  } else {
    // Step 6.a: NaN == NaN
    return x !== x && y !== y
  }
}
/*eslint-enable no-self-compare*/

function createChainableTypeChecker(validate) {
  function checkType(
    isRequired,
    props,
    propName,
    componentName,
    location,
    propFullName
  ) {
    componentName = componentName || ANONYMOUS
    propFullName = propFullName || propName
    if (props[propName] == null) {
      var locationName = ReactPropTypeLocationNames[location]
      if (isRequired) {
        return new Error(
          'Required ' +
            locationName +
            ' `' +
            propFullName +
            '` was not specified in ' +
            ('`' + componentName + '`.')
        )
      }
      return null
    } else {
      return validate(props, propName, componentName, location, propFullName)
    }
  }

  var chainedCheckType = checkType.bind(null, false)
  chainedCheckType.isRequired = checkType.bind(null, true)

  return chainedCheckType
}

function createPrimitiveTypeChecker(expectedType) {
  function validate(props, propName, componentName, location, propFullName) {
    var propValue = props[propName]
    var propType = getPropType(propValue)
    if (propType !== expectedType) {
      var locationName = ReactPropTypeLocationNames[location]
      // `propValue` being instance of, say, date/regexp, pass the 'object'
      // check, but we can offer a more precise error message here rather than
      // 'of type `object`'.
      var preciseType = getPreciseType(propValue)

      return new Error(
        'Invalid ' +
          locationName +
          ' `' +
          propFullName +
          '` of type ' +
          ('`' +
            preciseType +
            '` supplied to `' +
            componentName +
            '`, expected ') +
          ('`' + expectedType + '`.')
      )
    }
    return null
  }
  return createChainableTypeChecker(validate)
}

function createAnyTypeChecker() {
  return createChainableTypeChecker(emptyFunction.thatReturns(null))
}

function createArrayOfTypeChecker(typeChecker) {
  function validate(props, propName, componentName, location, propFullName) {
    if (typeof typeChecker !== 'function') {
      return new Error(
        'Property `' +
          propFullName +
          '` of component `' +
          componentName +
          '` has invalid PropType notation inside arrayOf.'
      )
    }
    var propValue = props[propName]
    if (!Array.isArray(propValue)) {
      var locationName = ReactPropTypeLocationNames[location]
      var propType = getPropType(propValue)
      return new Error(
        'Invalid ' +
          locationName +
          ' `' +
          propFullName +
          '` of type ' +
          ('`' +
            propType +
            '` supplied to `' +
            componentName +
            '`, expected an array.')
      )
    }
    for (var i = 0; i < propValue.length; i++) {
      var error = typeChecker(
        propValue,
        i,
        componentName,
        location,
        propFullName + '[' + i + ']'
      )
      if (error instanceof Error) {
        return error
      }
    }
    return null
  }
  return createChainableTypeChecker(validate)
}

function createElementTypeChecker() {
  function validate(props, propName, componentName, location, propFullName) {
    if (!ReactElement.isValidElement(props[propName])) {
      var locationName = ReactPropTypeLocationNames[location]
      return new Error(
        'Invalid ' +
          locationName +
          ' `' +
          propFullName +
          '` supplied to ' +
          ('`' + componentName + '`, expected a single ReactElement.')
      )
    }
    return null
  }
  return createChainableTypeChecker(validate)
}

function createInstanceTypeChecker(expectedClass) {
  function validate(props, propName, componentName, location, propFullName) {
    if (!(props[propName] instanceof expectedClass)) {
      var locationName = ReactPropTypeLocationNames[location]
      var expectedClassName = expectedClass.name || ANONYMOUS
      var actualClassName = getClassName(props[propName])
      return new Error(
        'Invalid ' +
          locationName +
          ' `' +
          propFullName +
          '` of type ' +
          ('`' +
            actualClassName +
            '` supplied to `' +
            componentName +
            '`, expected ') +
          ('instance of `' + expectedClassName + '`.')
      )
    }
    return null
  }
  return createChainableTypeChecker(validate)
}

function createEnumTypeChecker(expectedValues) {
  if (!Array.isArray(expectedValues)) {
    return createChainableTypeChecker(function () {
      return new Error(
        'Invalid argument supplied to oneOf, expected an instance of array.'
      )
    })
  }

  function validate(props, propName, componentName, location, propFullName) {
    var propValue = props[propName]
    for (var i = 0; i < expectedValues.length; i++) {
      if (is(propValue, expectedValues[i])) {
        return null
      }
    }

    var locationName = ReactPropTypeLocationNames[location]
    var valuesString = JSON.stringify(expectedValues)
    return new Error(
      'Invalid ' +
        locationName +
        ' `' +
        propFullName +
        '` of value `' +
        propValue +
        '` ' +
        ('supplied to `' +
          componentName +
          '`, expected one of ' +
          valuesString +
          '.')
    )
  }
  return createChainableTypeChecker(validate)
}

function createObjectOfTypeChecker(typeChecker) {
  function validate(props, propName, componentName, location, propFullName) {
    if (typeof typeChecker !== 'function') {
      return new Error(
        'Property `' +
          propFullName +
          '` of component `' +
          componentName +
          '` has invalid PropType notation inside objectOf.'
      )
    }
    var propValue = props[propName]
    var propType = getPropType(propValue)
    if (propType !== 'object') {
      var locationName = ReactPropTypeLocationNames[location]
      return new Error(
        'Invalid ' +
          locationName +
          ' `' +
          propFullName +
          '` of type ' +
          ('`' +
            propType +
            '` supplied to `' +
            componentName +
            '`, expected an object.')
      )
    }
    for (var key in propValue) {
      if (propValue.hasOwnProperty(key)) {
        var error = typeChecker(
          propValue,
          key,
          componentName,
          location,
          propFullName + '.' + key
        )
        if (error instanceof Error) {
          return error
        }
      }
    }
    return null
  }
  return createChainableTypeChecker(validate)
}

function createUnionTypeChecker(arrayOfTypeCheckers) {
  if (!Array.isArray(arrayOfTypeCheckers)) {
    return createChainableTypeChecker(function () {
      return new Error(
        'Invalid argument supplied to oneOfType, expected an instance of array.'
      )
    })
  }

  function validate(props, propName, componentName, location, propFullName) {
    for (var i = 0; i < arrayOfTypeCheckers.length; i++) {
      var checker = arrayOfTypeCheckers[i]
      if (
        checker(props, propName, componentName, location, propFullName) == null
      ) {
        return null
      }
    }

    var locationName = ReactPropTypeLocationNames[location]
    return new Error(
      'Invalid ' +
        locationName +
        ' `' +
        propFullName +
        '` supplied to ' +
        ('`' + componentName + '`.')
    )
  }
  return createChainableTypeChecker(validate)
}

function createNodeChecker() {
  function validate(props, propName, componentName, location, propFullName) {
    if (!isNode(props[propName])) {
      var locationName = ReactPropTypeLocationNames[location]
      return new Error(
        'Invalid ' +
          locationName +
          ' `' +
          propFullName +
          '` supplied to ' +
          ('`' + componentName + '`, expected a ReactNode.')
      )
    }
    return null
  }
  return createChainableTypeChecker(validate)
}

function createShapeTypeChecker(shapeTypes) {
  function validate(props, propName, componentName, location, propFullName) {
    var propValue = props[propName]
    var propType = getPropType(propValue)
    if (propType !== 'object') {
      var locationName = ReactPropTypeLocationNames[location]
      return new Error(
        'Invalid ' +
          locationName +
          ' `' +
          propFullName +
          '` of type `' +
          propType +
          '` ' +
          ('supplied to `' + componentName + '`, expected `object`.')
      )
    }
    for (var key in shapeTypes) {
      var checker = shapeTypes[key]
      if (!checker) {
        continue
      }
      var error = checker(
        propValue,
        key,
        componentName,
        location,
        propFullName + '.' + key
      )
      if (error) {
        return error
      }
    }
    return null
  }
  return createChainableTypeChecker(validate)
}

function isNode(propValue) {
  switch (typeof propValue) {
    case 'number':
    case 'string':
    case 'undefined':
      return true
    case 'boolean':
      return !propValue
    case 'object':
      if (Array.isArray(propValue)) {
        return propValue.every(isNode)
      }
      if (propValue === null || ReactElement.isValidElement(propValue)) {
        return true
      }

      var iteratorFn = getIteratorFn(propValue)
      if (iteratorFn) {
        var iterator = iteratorFn.call(propValue)
        var step
        if (iteratorFn !== propValue.entries) {
          while (!(step = iterator.next()).done) {
            if (!isNode(step.value)) {
              return false
            }
          }
        } else {
          // Iterator will provide entry [k,v] tuples rather than values.
          while (!(step = iterator.next()).done) {
            var entry = step.value
            if (entry) {
              if (!isNode(entry[1])) {
                return false
              }
            }
          }
        }
      } else {
        return false
      }

      return true
    default:
      return false
  }
}

// Equivalent of `typeof` but with special handling for array and regexp.
function getPropType(propValue) {
  var propType = typeof propValue
  if (Array.isArray(propValue)) {
    return 'array'
  }
  if (propValue instanceof RegExp) {
    // Old webkits (at least until Android 4.0) return 'function' rather than
    // 'object' for typeof a RegExp. We'll normalize this here so that /bla/
    // passes PropTypes.object.
    return 'object'
  }
  return propType
}

// This handles more types than `getPropType`. Only used for error messages.
// See `createPrimitiveTypeChecker`.
function getPreciseType(propValue) {
  var propType = getPropType(propValue)
  if (propType === 'object') {
    if (propValue instanceof Date) {
      return 'date'
    } else if (propValue instanceof RegExp) {
      return 'regexp'
    }
  }
  return propType
}

// Returns class name of the object, if any.
function getClassName(propValue) {
  if (!propValue.constructor || !propValue.constructor.name) {
    return ANONYMOUS
  }
  return propValue.constructor.name
}

module.exports = ReactPropTypes
